package roborally.Pieces;

import java.util.*;
import be.kuleuven.cs.som.annotate.*;
import roborally.Board;
import roborally.MoveOutOfBoardException;

/**
 * This is a class of Positions, which have a x- and y-coordinate and a Board. An extended instance of HashSet guarantees that all 
 * Positions are unique to avoid memory errors. This is done so that (pos1 == pos2) == (pos1.equals(pos2) ) for any Position pos1 or pos2.
 * @invar | this.isValidPosition(this.getBoard(), this.getX(), this.getY() )
 * @version 2.0
 * @author Bart Opsomer 2de Bachelor Ingenieurswetenschappen Electrotechniek-Computerwetenschappen 
 * 		   Peter Opsomer 1ste Master Wiskundige IngenieursTechnieken
 */
@Value
public class Position {
	
	private final long x;
	private final long y;
	private final Board board;
	
	private static Position.ExtendedHashSet positions = new Position.ExtendedHashSet();
	
	private static class ExtendedHashSet extends HashSet<Position> implements java.lang.Iterable<Position>{
		// Get the unique position corresponding with (x,y) on board
		private Position getPositionAt(long x, long y, Board board) throws MoveOutOfBoardException {
			for(Position pos : this) {
				if(pos.equals(x,y,board) ) {
					return pos;
				}
			}
			Position newPos =  new Position(x,y,board);
			this.add(newPos);
			return newPos;
		}
		private static final long serialVersionUID = 1L;
	}
	/**
	 * Create a new Position on the Board.
	 * 
	 * @param x the x-coordinate of the Position
	 * @param y the y-coordinate of the Position
	 * @param board the Board of the Position
	 * @post | new.getX() == x
	 * @post | new.getY() == Y
	 * @post | new.getBoard().equals(board)
	 * @throws IllegalArgumentException
	 * 		| !isValidPosition(board, x,y )
	 */
	@Raw
	private Position(long x, long y,Board board) throws MoveOutOfBoardException {
		if(!isValidPosition(board, x, y)) {
			throw new MoveOutOfBoardException();
		}
		this.x = x;
		this.y = y;
		this.board = board;
	}
	/**
	 * Return the desired Position. It is created if it doesn't exist yet. Else, the existing Position is returned.
	 * 
	 * @param x the x-coordinate of the Position
	 * @param y the y-coordinate of the Position
	 * @param board the Board of the Position
	 * @return | result.getX() == x
	 * @return | result.getY() == Y
	 * @return | result.getBoard() == board
	 * @throws MoveOutOfBoardException
	 * 		| !isValidPosition(board, x,y )
	 */
	public static Position getPosition(long x, long y, Board board)  throws MoveOutOfBoardException {
		 return positions.getPositionAt(x,y,board);
	 }
	/**
	 * Get whether the Position denoted by the arguments would be a valid Position.
	 * @param board the Board of the Position
	 * @param x the x-coordinate of the Position
	 * @param y the y-coordinate of the Position
	 * @return  | result == board.getWidth() > x && board.getHeight() > y && x>=0 && y>=0
	 */
	public static boolean isValidPosition(Board board, long x, long y) {
		return board.getWidth() > x && board.getHeight() > y && x>=0 && y>=0;
	}
	/**
	 * @return the Board of this Position
	 */
	@Basic @Immutable
	public Board getBoard() {
		return this.board;
	}
	/**
	 * @return the x-coordinate of this Position
	 */
	@Basic @Immutable
	public long getX() {
		return x;
	}
	/**
	 * @return the y-coordinate of this Position
	 */
	@Basic @Immutable
	public long getY() {
		return y;
	}
	/**
	 * Get the Position on the given Board with the same coordinates. This method is used for merging and throws an Exception when for example a 
	 * board that is too small is given.
	 * 
	 * @param board the Board to transfer the coordinates to
	 * @return | result.getBoard().equals(board)
	 * @return | result.getX().equals(this.getX() )
	 * @return | result.getY().equals(this.getY() )
	 * @throws MoveOutOfBoardException
	 * 		| !isValidPosition(board,getX(), getY() ) || board.isTerminated()
	 */
	public Position getSamePositionOnOtherBoard(Board board) throws MoveOutOfBoardException{
		if(!isValidPosition(board,x,y) || board.isTerminated()) {
			throw new MoveOutOfBoardException();
		}
		return Position.getPosition(this.getX(), this.getY(), board);
	}
	/**
	 * Return whether this Position is equal to the given Object.
	 * 
	 * @effect | if(other instanceof Position) then 
	 * 		   |	result ==  equals(second.x,second.y,second.board)
	 * 		   | else
	 * 		   |	result == false
	 */
	@Override
	public boolean equals(Object other) {
		if(other instanceof Position) {
			Position second = (Position) other;
			return equals(second.x,second.y,second.board);
		}
		return false;
	}
	/**
	 * Return whether this Position is equal to the Position denoted by the arguments.
	 * 
	 * @param x the x-coordinate of the Position
	 * @param y the y-coordinate of the Position
	 * @param board the Board of the Position
	 * @return | (x == this.getX() && y == this.getY() && board == this.getBoard() )
	 */
	public boolean equals(long x, long y, Board board) {
		return (x == this.x && y == this.y && board == this.board);
	}
	/**
	 * Return the Manhattan Distance of this Position to the given Position.
	 * 
	 * @param other the Position to measure the Manhattan distance to
	 * @return abs(getX()-other.getX() ) + abs(getY()-other.getY() )
	 */
	public long getManhattanDistanceTo(Position other) {
		return Math.abs(x-other.x) + Math.abs(y-other.y);
	}

}
